package junior.db.query;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import junior.util.reflect.ReflectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.util.ClassUtils;

import javax.persistence.Transient;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.regex.Pattern;

/**
 * @author WuBo
 * @CreateDate 2011-12-5 下午02:20:36
 */
public class QueryHelper {
    /**
     * LIKE查询关键字验证包含特殊字符表达式
     */
    public final static String HAS_SPECIAL_EL = "^.*?[%\\\\_]+.*?$";
    public final static char[] LIKE_FILTER_CHAR = new char[]{'%', '\\', '_'};
    public final static Pattern FIELD_REX = Pattern.compile("[a-zA-Z][a-zA-Z_.0-9]+");
    private static final ConcurrentMap<Class<?>, Pair<String, Class<?>>[]> dbFieldCache =
            new ConcurrentHashMap<>(64);
    
    public static boolean hasSpecialChar(String param) {
        return param.matches(HAS_SPECIAL_EL);
    }
    
    public static String likeLR(String param) {
        return "%" + convert(param) + "%";
    }
    
    public static String likeL(String param) {
        return "%" + convert(param);
    }
    
    public static String likeR(String param) {
        return convert(param) + "%";
    }
    
    public static boolean isField(String field) {
        return FIELD_REX.matcher(field).matches();
    }
    
    public static void checkField(String field) {
        if (!FIELD_REX.matcher(field).matches()) {
            throw new IllegalArgumentException("Illegal field: " + field);
        }
    }
    
    public static String convert(String param) {
        StringBuilder result = new StringBuilder("");
        if (param != null && param.trim().length() > 0) {
            if (!hasSpecialChar(param)) {
                result.append(param);
            } else {
                char[] chars = param.toCharArray();
                for (char c : chars) {
                    boolean sc = false;
                    for (char f : LIKE_FILTER_CHAR) {
                        if (c == f) {
                            sc = true;
                            break;
                        }
                    }
                    if (sc) {
                        result.append("\\" + c);
                    } else {
                        result.append(c);
                    }
                }
            }
        }
        return result.toString();
    }
    
    public static List<Pair<String, Class<?>>> getDBFieldNameAndTypes(Class<?> entityClass, String prefix, String... excludeFields) {
        List<Pair<String, Class<?>>> resultFields = Lists.newArrayList();
        doGetDBFieldNames(entityClass, prefix, (pair) -> {
            resultFields.add(pair);
        }, excludeFields);
        return resultFields;
    }
    
    public static String[] getDBFieldNames(Class<?> entityClass, String prefix, String... excludeFields) {
        List<String> resultFields = Lists.newArrayList();
        doGetDBFieldNames(entityClass, prefix, (pair) -> {
            resultFields.add(pair.getLeft());
        }, excludeFields);
        return resultFields.toArray(new String[0]);
    }
    
    private static void doGetDBFieldNames(Class<?> entityClass, String prefix, Consumer<Pair<String, Class<?>>> onField, String... excludeFields) {
        @SuppressWarnings("unchecked")
        List<String> excludeProps = Collections.EMPTY_LIST;
        if (excludeFields != null) {
            excludeProps = Arrays.asList(excludeFields);
        }
        Pair<String, Class<?>>[] result = dbFieldCache.get(entityClass);
        if (result == null) {
            PropertyDescriptor[] propertyDescriptors = ReflectUtils.getPropertyDescriptors(entityClass);
            List<Pair<String, Class<?>>> fields = new java.util.ArrayList<>();
            for (PropertyDescriptor pd : propertyDescriptors) {
                Method getter = pd.getReadMethod();
                if (pd.getWriteMethod() == null || getter == null) {
                    continue;
                }
                Class<? extends Annotation> jsr220TransientClass = getJsr220TransientClass();
                try {
                    Field f = ReflectUtils.getDeclaredField(entityClass, pd.getName(), true);
                    if (f == null) {
                        continue;
                    }
                    if (Modifier.isTransient(f.getModifiers())) {
                        continue;
                    }
                    if (jsr220TransientClass != null) {
                        if (f.getAnnotation(jsr220TransientClass) != null) {
                            continue;
                        }
                    }
                } catch (NoSuchFieldException ignore) {
                    // do nothing
                }
                
                if (getter.getAnnotation(Transient.class) != null) {
                    continue;
                }
                if (jsr220TransientClass != null) {
                    if (getter.getAnnotation(jsr220TransientClass) != null) {
                        continue;
                    }
                }
                if (isDBField(pd.getPropertyType())) {
                    String property = pd.getName();
                    String field = Character.isUpperCase(property.charAt(0)) ? new StringBuffer(property.length()).append(Character.toLowerCase(property.charAt(0))).append(property.substring(1)).toString() : property;
                    fields.add(Pair.of(field, pd.getPropertyType()));
                }
            }
            result = fields.toArray(new Pair[0]);
        }
        dbFieldCache.putIfAbsent(entityClass, result);
        
        String p = prefix != null ? prefix : "";
        for (Pair<String, Class<?>> pair : result) {
            if (!excludeProps.contains(pair.getLeft())) {
                onField.accept(Pair.of(p + pair.getLeft(), pair.getRight()));
            }
        }
    }
    
    @SuppressWarnings("unchecked")
    private static Class<? extends Annotation> getJsr220TransientClass() {
        try {
            return (Class<? extends Annotation>) Class.forName("javax.persistence.Transient");
        } catch (ClassNotFoundException e) {
            return null;
        }
    }
    
    private static boolean isDBField(Class<?> clazz) {
        return ClassUtils.isPrimitiveOrWrapper(clazz)
                || String.class.isAssignableFrom(clazz)
                || Number.class.isAssignableFrom(clazz)
                || Date.class.isAssignableFrom(clazz)
                || Calendar.class.isAssignableFrom(clazz)
                || Temporal.class.isAssignableFrom(clazz)
                || clazz.isEnum();
    }
    
    public static boolean hasFunction(String column) {
        return column.contains("(") && column.contains(")");
    }
    
    public static boolean hasPrefix(String column) {
        return column.indexOf(".") != -1;
    }
    
    /**
     * 将命名参数查询转换成普通占位符参数查询
     * @param sql
     * @param paramNames
     * @param values
     * @return
     */
    public static NamedParamResult parseNamedParams(String sql, String[] paramNames, Object[] values) {
        if (paramNames.length < 1) {
            return new NamedParamResult(sql, values);
        }
        Map<String, Object> map = new HashMap<String, Object>(paramNames.length);
        for (int i = 0; i < paramNames.length; i++) {
            map.put(paramNames[i], values[i]);
        }
        List<Object> valueList = new LinkedList<Object>();
        
        Map<Integer, String> sortMap = new TreeMap<Integer, String>();
        
        for (String paramName : paramNames) {
            int idx = -1;
            int fromIndex = 0;
            while ((idx = sql.indexOf(":" + paramName, fromIndex)) != -1) {
                sortMap.put(idx, paramName);
                fromIndex = idx + paramName.length();
            }
        }
        for (Integer key : sortMap.keySet()) {
            valueList.add(map.get(sortMap.get(key)));
        }
        
        if (valueList.size() < 1) {
            return new NamedParamResult(sql, values);
        }
        return new NamedParamResult(sql.replaceAll("\\:\\w+", "?"), valueList.toArray());
    }
    
    
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> listMap2Map(List<Map<String, Object>> list, String keyColumn, String valColumn) {
        Map<K, V> rslt = Maps.newHashMapWithExpectedSize(list.size());
        for (Map<String, Object> row : list) {
            rslt.put((K) row.get(keyColumn), (V) row.get(valColumn));
        }
        return rslt;
    }
    
    @SuppressWarnings("unchecked")
    public static <K> Map<K, Object[]> listMap2Map(List<Map<String, Object>> list, String keyColumn, String... valColumn) {
        Map<K, Object[]> rslt = Maps.newHashMapWithExpectedSize(list.size());
        for (Map<String, Object> row : list) {
            K k = (K) row.get(keyColumn);
            Object[] val = new Object[valColumn.length];
            for (int i = 0; i < val.length; i++) {
                val[i] = row.get(valColumn[i]);
            }
            rslt.put(k, val);
        }
        return rslt;
    }
    
    public static class NamedParamResult {
        private String sql;
        private Object[] values;
        
        private NamedParamResult(String sql, Object[] values) {
            this.sql = sql;
            this.values = values;
        }
        
        public String getSql() {
            return this.sql;
        }
        
        public Object[] getValues() {
            return this.values;
        }
    }
    
}
